using System;
using System.IO;
using System.Collections.Generic;
using System.Windows;
using System.Net;
using System.ComponentModel;
using System.Diagnostics;
using System.Threading;

using FlickrNet;
using FlickrApi = FlickrNet.Flickr;

using Yorrick.FlickrMetadataSynchr.Data;
using Yorrick.FlickrMetadataSynchr.Events;
using Yorrick.FlickrMetadataSynchr.Properties;

namespace Yorrick.FlickrMetadataSynchr.Flickr
{
    /// <summary>
    /// Class that helps with Flickr related stuff.
    /// </summary>
    internal sealed class FlickrHelper : INotifyPropertyChanged
    {
        #region Nested delegates
        /// <summary>
        /// Delegate type for a factory method for creating an async worker instance.
        /// </summary>
        /// <typeparam name="TIn">The "in" metadata type for the worker.</typeparam>
        /// <typeparam name="TOut">The "out" metadata type for the worker.</typeparam>
        /// <returns></returns>
        private delegate AsyncFlickrWorker<TIn, TOut> CreateAsyncFlickrWorker<TIn, TOut>();
        #endregion

        #region Private constants
        /// <summary>
        /// The maximum for the maximum number of pictures to fetch in a single call to the Flickr API.
        /// </summary>
        private const int _MaximumFlickrBatchSize = 500;

        /// <summary>
        /// The maximum number of HTTP connections to Flickr.
        /// </summary>
        private const int _MaximumConnectionsToFlickr = 10;
        #endregion

        #region Private fields
        /// <summary>
        /// The Flickr API object.
        /// </summary>
        private FlickrApi _flickrApi;

        /// <summary>
        /// The OAuth request token.
        /// </summary>
        private OAuthRequestToken _requestToken;

        /// <summary>
        /// Whether or not the application has an authorized connection to Flickr.
        /// </summary>
        private bool _isConnected;

        /// <summary>
        /// Whether or not this instance is busy connecting to Flickr.
        /// </summary>
        private bool _busyConnecting;

        /// <summary>
        /// The Flickr id of the user.
        /// </summary>
        private string _userId;

        /// <summary>
        /// Information about the Flickr user.
        /// </summary>
        private Person _currentUser;

        /// <summary>
        /// The object to lock on when connecting to Flickr.
        /// </summary>
        private object _connectLock = new Object();

        /// <summary>
        /// The list of photosets.
        /// </summary>
        private List<Photoset> _photosets;

        /// <summary>
        /// Whether or not to simulate updating metadata.
        /// </summary>
        private bool _simulateUpdatingMetadata;

        /// <summary>
        /// The maximum number of pictures to fetch in a single call to the Flickr API. The maximum is <see cref="FlickrHelper._MaximumFlickrBatchSize"/>.
        /// </summary>
        private int _flickrBatchSize;

        /// <summary>
        /// The maximum number of async workers to instantiate.
        /// </summary>
        private int _maximumNumberOfAsyncWorkers;

        /// <summary>
        /// The number of pictures above which an additional async worker should be used.
        /// </summary>
        private int _asyncWorkerPictureThreshold;
        #endregion

        #region Public properties
        /// <summary>
        /// Gets or sets whether or not to simulate updating metadata.
        /// </summary>
        public bool SimulateUpdatingMetadata
        {
            get
            {
                return _simulateUpdatingMetadata;
            }
            set
            {
                _simulateUpdatingMetadata = value;
            }
        }

        /// <summary>
        /// Gets the currently active Flickr user. The value is null if there is no active user.
        /// </summary>
        public Person CurrentUser
        {
            get
            {
                if (_isConnected)
                {
                    if (_currentUser == null)
                    {
                        _currentUser = _flickrApi.PeopleGetInfo(_userId);
                    }
                    return _currentUser;
                }
                else
                {
                    return null;
                }
            }
        }

        /// <summary>
        /// Gets the name of the currently active Flickr user. 
        /// The value is null if there is no active user.
        /// </summary>
        public string UserName
        {
            get
            {
                Person currentUser = CurrentUser;
                if (currentUser == null)
                {
                    return null;
                }
                else if (!String.IsNullOrEmpty(currentUser.RealName))
                {
                    return currentUser.RealName;
                }
                else
                {
                    return currentUser.UserName;
                }
            }
        }

        /// <summary>
        /// Gets if the user has a Flickr Pro account.
        /// </summary>
        public bool IsFlickrProUser
        {
            get
            {
                Person currentUser = CurrentUser;
                if (currentUser == null)
                {
                    return false;
                }
                else
                {
                    return currentUser.IsPro;
                }
            }
        }

        /// <summary>
        /// Gets whether the application has an authorization token for the user.
        /// </summary>
        /// <remarks>The validity of the token is not checked.</remarks>
        public bool HasAuthorizationToken
        {
            get
            {
                return !String.IsNullOrEmpty(this.AuthorizationToken) && !String.IsNullOrEmpty(this.AuthorizationTokenSecret);
            }
        }

        /// <summary>
        /// Gets whether or not the application has an authorized connection to Flickr.
        /// </summary>
        public bool IsConnected
        {
            get
            {
                return _isConnected;
            }
            private set
            {
                bool oldValue = _isConnected;
                _isConnected = value;

                if (_isConnected)
                {
                    LoadPhotosetsAsync();
                }
                else
                {
                    _flickrApi = null;
                    _userId = null;
                    _currentUser = null;
                    SetPhotosets(null);
                }

                if (_isConnected != oldValue)
                {
                    NotifyPropertyChanged("IsConnected");
                    NotifyPropertyChanged("CurrentUser");
                    NotifyPropertyChanged("UserName");
                    NotifyPropertyChanged("IsFlickrProUser");
                    NotifyPropertyChanged("CanConnect");
                }
            }
        }

        /// <summary>
        /// Gets whether or not the user can connect to Flickr at this time.
        /// </summary>
        public bool CanConnect
        {
            get
            {
                return (!IsConnected && !BusyConnecting && HasAuthorizationToken);
            }
        }

        /// <summary>
        /// Gets whether or not the application can forget the authorization token at this time.
        /// </summary>
        public bool CanForgetAuthorizationToken
        {
            get
            {
                return (HasAuthorizationToken && !BusyConnecting);
            }
        }


        /// <summary>
        /// Gets or sets whether or not this instance is busy connecting to Flickr.
        /// </summary>
        public bool BusyConnecting
        {
            get
            {
                return _busyConnecting;
            }
            private set
            {
                _busyConnecting = value;

                NotifyPropertyChanged("BusyConnecting");
                NotifyPropertyChanged("CanConnect");
            }
        }

        /// <summary>
        /// Gets the list of photosets of the current user.
        /// </summary>
        /// <remarks>The list is sorted on the title of the set.</remarks>
        public IList<Photoset> Photosets
        {
            get
            {
                return _photosets;
            }
        }
        #endregion

        #region Private properties
        /// <summary>
        /// Gets or sets the authorization token that gives the application access on behalf of the
        /// user on Flickr.
        /// </summary>
        /// <value>The Flickr authorization token or <c>null</c> when there is no token.</value>
        /// <remarks>This token is persisted in the user settings across application
        /// sessions. If the authorization token is set to <c>null</c> the application is no longer connected.</remarks>
        private string AuthorizationToken
        {
            get
            {
                return Settings.Default.FlickrAuthToken;
            }
            set
            {
                Settings.Default.FlickrAuthToken = value;
                Settings.Default.Save();

                IsConnected = false;

                NotifyPropertyChanged("HasAuthorizationToken");
                NotifyPropertyChanged("CanConnect");
            }
        }

        private string AuthorizationTokenSecret
        {
            get
            {
                return Settings.Default.FlickrAuthTokenSecret;
            }
            set
            {
                Settings.Default.FlickrAuthTokenSecret = value;
                Settings.Default.Save();

                IsConnected = false;

                NotifyPropertyChanged("HasAuthorizationToken");
                NotifyPropertyChanged("CanConnect");
            }
        }
        #endregion

        #region Constructor
        /// <summary>
        /// Creates a new instance.
        /// </summary>
        public FlickrHelper()
        {
            InitializeSettings();
        }
        #endregion

        #region Public methods
        /// <summary>
        /// Tries to connect to Flickr and returns whether or not this was successful.
        /// </summary>
        /// <returns>Whether or not the connection to Flickr was made successfully.</returns>
        public bool TryConnect()
        {
            lock (_connectLock)
            {
                BusyConnecting = true;

                try
                {
                    IsConnected = Connect();

                    return IsConnected;
                }
                catch
                {
                    IsConnected = false;
                    throw;
                }
                finally
                {
                    BusyConnecting = false;
                }
            }
        }

        /// <summary>
        /// Forgets the authorization token.
        /// </summary>
        public void ForgetAuthorizationToken()
        {
            this.AuthorizationToken = null;
        }

        /// <summary>
        /// Requests authorization through the user for this app to access the user's photos on Flickr.
        /// </summary>
        /// <param name="owner">The owner window.</param>
        /// <param name="title">The title of the message box.</param>
        /// <returns><c>True</c> when the user has acknowledged giving the application authorization,
        /// <c>false</c> otherwise.</returns>
        /// <exception cref="FlickrHelperException">When authorization fails.</exception>
        public bool RequestAuthorizationThroughUser(Window owner, string title)
        {
            try
            {
                _flickrApi = new FlickrApi(Settings.Default.FlickrApiKey,
                                        Settings.Default.FlickrSharedSecret,
                                        null);

                InitializeFlickrApi();

                _requestToken = _flickrApi.OAuthGetRequestToken("oob");

                string url = _flickrApi.OAuthCalculateAuthorizationUrl(_requestToken.Token, AuthLevel.Write);

                Trace.TraceInformation("Opening browser window with URL {0}.", url);

                // Open a web browser in which the user should authorize this application.
                System.Diagnostics.Process.Start(url);

                // Wait for the user to confirm that the authorization procedure is complete.
                string message = Messages.RequestFlickrAuthorization.Replace("\\n", "\n");

                var verificationWindow = new VerificationWindow();

                bool? dialogBoxResult = verificationWindow.ShowDialog();

                bool userAcknowledged = (dialogBoxResult.HasValue && dialogBoxResult.Value == true);

                Trace.TraceInformation("Popped up dialog box to the user to acknowledge authorizing the application and enter the verification code. Owner is {2}. Dialog box result is {0}. This means user acknowledged {1}.", dialogBoxResult, userAcknowledged, owner);

                if (userAcknowledged)
                {
                    OAuthAccessToken accessToken = _flickrApi.OAuthGetAccessToken(_requestToken, verificationWindow.VerificationCodeTextBox.Text);

                    AuthorizationToken = accessToken.Token;
                    AuthorizationTokenSecret = accessToken.TokenSecret;
                }

                return userAcknowledged;
            }
            catch (WebException ex)
            {
                throw FlickrHelperException.GetFailedToConnectToFlickrException(ex);
            }
            catch (OAuthException ex)
            {
                throw FlickrHelperException.GetFailedToConnectToFlickrException(ex);
            }
            catch (FlickrException ex)
            {
                throw HandleFlickrException(ex, Messages.ApplicationNotAuthorized);
            }
        }

        /// <summary>
        /// Flushes the cache of the Flickr API library.
        /// </summary>
        public void FlushCache()
        {
            FlickrApi.FlushCache();
        }

        /// <summary>
        /// Returns the list of all photos for the specified photoset.
        /// </summary>
        /// <param name="photoset">The photoset for which to return all photos.</param>
        /// <returns>The list of all photos in the set.</returns>
        /// <remarks>The list is sorted on date taken.</remarks>
        public IList<Photo> GetPhotos(Photoset photoset)
        {
            EnsureConnected();

            try
            {
                Photoset photosetWithoutPhotos = _flickrApi.PhotosetsGetInfo(photoset.PhotosetId);

                int numberOfPhotos = photosetWithoutPhotos.NumberOfPhotos;

                List<Photo> photos = new List<Photo>(numberOfPhotos);

                int numberOfPages = (numberOfPhotos / _flickrBatchSize) + 1;

                Trace.TraceInformation("Flickr photoset has {0} pictures. Will fetch info in {1} batches.", numberOfPhotos, numberOfPages);

                for (int pageNumber = 1; pageNumber <= numberOfPages; pageNumber++)
                {
                    PhotosetPhotoCollection photosetWithPhotos = _flickrApi.PhotosetsGetPhotos(photoset.PhotosetId, PhotoSearchExtras.DateTaken, PrivacyFilter.None, pageNumber, _flickrBatchSize);

                    Trace.TraceInformation("Fetched {0} pictures in batch #{1}.", photosetWithPhotos.Count, pageNumber);

                    photos.AddRange(photosetWithPhotos);
                }

                photos.Sort(delegate(Photo photo1, Photo photo2)
                   {
                       return photo1.DateTaken.CompareTo(photo2.DateTaken);
                   });

                return photos.AsReadOnly();
            }
            catch (WebException ex)
            {
                throw FlickrHelperException.GetFailedToConnectToFlickrException(ex);
            }
            catch (FlickrException ex)
            {
                throw HandleFlickrException(ex, Messages.GetPhotosFailed);
            }
        }

        /// <summary>
        /// Returns the photoset with the specified id or <c>null</c> if
        /// there is no such photoset.
        /// </summary>
        /// <param name="photosetId">The id of the photoset.</param>
        /// <returns>The photoset with the specified id or <c>null</c> if
        /// there is no such photoset.</returns>
        /// <remarks>Only photosets belonging to the user are considered.</remarks>
        public Photoset GetPhotosetById(string photosetId)
        {
            if (Photosets == null)
            {
                return null;
            }

            Photoset foundPhotoset = _photosets.Find(delegate(Photoset set)
                              {
                                  return (set.PhotosetId == photosetId);
                              });

            return foundPhotoset;
        }

        /// <summary>
        /// Updates the metadata in Flickr pictures.
        /// </summary>
        /// <param name="updatedFlickrPictures">The Flickr pictures that need updating.</param>
        /// <param name="progressCallback">A callback to receive progress information. Is allowed to be null.</param>
        public void UpdatePictureMetadata(IList<FlickrPictureMetadata> updatedFlickrPictures, EventHandler<PictureProgressEventArgs> progressCallback)
        {
            if (_simulateUpdatingMetadata)
            {
                Trace.TraceInformation(Messages.SimulatingUpdatingFlickrPictures);
            }

            ProcessMetadataWithMultipleWorkers<FlickrPictureMetadata, FlickrPictureMetadata>(
                updatedFlickrPictures,
                progressCallback,
                delegate()
                {
                    return new AsyncMetadataUpdater(_flickrApi, _simulateUpdatingMetadata);
                });
        }

        /// <summary>
        /// Returns a list with info about every photo in <paramref name="photoList"/>.
        /// </summary>
        /// <param name="photoList">The list with <see cref="Photo"/>s to return info for.</param>
        /// <param name="progressCallback">A callback to receive progress information.</param>
        /// <returns>A list with info about every photo in <paramref name="photoList"/>.</returns>
        public IList<PhotoInfo> GetPhotoInfo(IList<Photo> photoList, EventHandler<PictureProgressEventArgs> progressCallback)
        {
            IList<PhotoInfo> photoInfoList =
                ProcessMetadataWithMultipleWorkers<Photo, PhotoInfo>(
                    photoList,
                    progressCallback,
                    delegate()
                    {
                        return new AsyncPhotoInfoFetcher(_flickrApi);
                    });

            return photoInfoList;
        }

        /// <summary>
        /// Downloads pictures from Flickr to a local folder. Returns a list of names of local files that
        /// were created.
        /// </summary>
        /// <param name="flickrPicturesToDownload">The list with pictures on Flickr to download.</param>
        /// <param name="progressCallback">A callback to receive progress information. Is allowed to be null.</param>
        /// <param name="shouldCancel">A delegate that can be called to check for cancellation.</param>
        /// <returns>A list of file names for the downloaded pictures. The items in the list are in one-to-one
        /// correspondence with the items in <paramref name="flickrPicturesToDownload"/>.
        /// Return <c>null</c> if the download is cancelled.</returns>
        public IList<string> DownloadPictures(IList<FlickrPictureMetadata> flickrPicturesToDownload, string localFolder, EventHandler<PictureProgressEventArgs> progressCallback, ShouldCancelDelegate shouldCancel)
        {
            EnsureConnected();

            List<string> fileNames = new List<string>(flickrPicturesToDownload.Count);

            int currentPhotoNumber = 1;
            int numberOfPicturesToDownload = flickrPicturesToDownload.Count;

            PictureDownloader pictureDownloader = new PictureDownloader();

            foreach (FlickrPictureMetadata flickrPictureMetadata in flickrPicturesToDownload)
            {
                if (progressCallback != null)
                {
                    PictureProgressEventArgs e = new PictureProgressEventArgs(currentPhotoNumber, numberOfPicturesToDownload);
                    progressCallback(this, e);
                    if (e.CancelProcess)
                    {
                        return null;
                    }
                }

                if (String.IsNullOrEmpty(flickrPictureMetadata.OriginalUrl))
                {
                    Trace.TraceWarning("Skipping download of Flickr picture for which the original URL is not available (id {0}).",
                       flickrPictureMetadata.Id);
                }
                else
                {
                    string fileNameWithPath = DownloadedPictureFilenameHelper.GetFilenameForDownload(localFolder, flickrPictureMetadata);

                    bool finishedDownloading = pictureDownloader.Download(flickrPictureMetadata.OriginalUrl, fileNameWithPath, shouldCancel);

                    // Make sure the downloaded file is marked as updated slighly earlier than the Flickr picture.
                    // This might cause problems in the next run of the app if the downloaded picture did not
                    // receive the metadata from the Flickr picture. This can happen if the sync process is aborted half way.
                    // If we mark the local file as updated slightly earlier, in the next run it will be updated
                    // with metadata from Flickr.
                    if (File.Exists(fileNameWithPath) && finishedDownloading)
                    {
                        File.SetLastWriteTimeUtc(fileNameWithPath, flickrPictureMetadata.LastUpdated.AddSeconds(-1.0));

                        bool isJpegFile = flickrPictureMetadata.OriginalUrl.EndsWith(".jpg");

                        if (isJpegFile)
                        {
                            fileNames.Add(fileNameWithPath);
                        }
                        else
                        {
                            Trace.TraceWarning("Skipping metadata synch for downloaded non-JPEG picture (id {0}).",
                                flickrPictureMetadata.Id);
                        }
                    }
                }

                currentPhotoNumber++;
            }

            return fileNames.AsReadOnly();
        }
        #endregion

        #region Private methods
        /// <summary>
        /// Helper method to set the <see cref="_photosets"/> field, while simultaneously
        /// raising a property changed notification for the <see cref="Photosets"/> property.
        /// </summary>
        /// <param name="photosets"></param>
        private void SetPhotosets(List<Photoset> photosets)
        {
            _photosets = photosets;
            NotifyPropertyChanged("Photosets");
        }

        /// <summary>
        /// Gets the <see cref="PhotoInfo"/> for the picture with the specified id.
        /// </summary>
        /// <param name="photoId">The id of the picture to fetch the <see cref="PhotoInfo"/> for.</param>
        /// <param name="photoSecret">The photo secret.</param>
        /// <returns>The <see cref="PhotoInfo"/> for the picture with the specified id.</returns>
        private PhotoInfo GetPhotoInfo(string photoId, string photoSecret)
        {
            PhotoInfo photoInfo;

            photoInfo = _flickrApi.PhotosGetInfo(photoId, photoSecret);

            return photoInfo;
        }

        /// <summary>
        /// Connects to Flickr.
        /// </summary>
        /// <returns><c>True</c> when successful, <c>false</c> otherwise.</returns>
        /// <remarks>This method should only be called when the connect lock is held.</remarks>
        private bool Connect()
        {
            if (!HasAuthorizationToken)
            {
                return false;
            }

            try
            {
                _flickrApi = new FlickrApi(Settings.Default.FlickrApiKey,
                                        Settings.Default.FlickrSharedSecret);

                InitializeFlickrApi();

                _flickrApi.OAuthAccessToken = AuthorizationToken;
                _flickrApi.OAuthAccessTokenSecret = Settings.Default.FlickrAuthTokenSecret;

                FoundUser user = _flickrApi.TestLogin();

                _userId = user.UserId;

                return true;
            }
            catch (WebException ex)
            {
                throw FlickrHelperException.GetFailedToConnectToFlickrException(ex);
            }
            catch (FlickrException ex)
            {
                FlickrHelperException wrappedException = HandleFlickrException(ex, Messages.ConnectFailed);

                throw wrappedException;
            }
        }

        /// <summary>
        /// Initializes settings on the FlickrApi object after it is constructed. Also intializes static settings.
        /// </summary>
        private void InitializeFlickrApi()
        {
            FlickrApi.CacheDisabled = true;

            try
            {
                _flickrApi.HttpTimeout = Settings.Default.FlickrNetHttpTimeoutInSeconds * 1000;
            }
            catch (InvalidCastException ex)
            {
                throw FlickrHelperException.GetInvalidConfigurationSettingException("FlickrNetHttpTimeoutInSeconds",
                   ex);
            }
        }


        /// <summary>
        /// Initializes the settings from configuration for this instance.
        /// </summary>
        private void InitializeSettings()
        {
            InitializeFlickrBatchSize();
            InitializeAsyncWorkerPictureThreshold();
            InitializeMaximumNumberOfAsyncWorkers();
        }

        /// <summary>
        /// Initializes the Flickr batch size setting.
        /// </summary>
        private void InitializeFlickrBatchSize()
        {
            try
            {
                _flickrBatchSize = Settings.Default.FlickrBatchSize;
            }
            catch (InvalidCastException ex)
            {
                throw FlickrHelperException.GetInvalidConfigurationSettingException("FlickrBatchSize", ex);
            }

            if (_flickrBatchSize < 0)
            {
                _flickrBatchSize = 0;
            }
            else if (_flickrBatchSize > _MaximumFlickrBatchSize)
            {
                _flickrBatchSize = _MaximumFlickrBatchSize;
            }
        }

        /// <summary>
        /// Initializes the async worker picture threshold setting.
        /// </summary>
        private void InitializeAsyncWorkerPictureThreshold()
        {
            try
            {
                _asyncWorkerPictureThreshold = Settings.Default.AsyncWorkerPictureThreshold;
            }
            catch (InvalidCastException ex)
            {
                throw FlickrHelperException.GetInvalidConfigurationSettingException("AsyncWorkerPictureThreshold", ex);
            }

            if (_asyncWorkerPictureThreshold < 1)
            {
                _asyncWorkerPictureThreshold = 1;
            }
        }

        /// <summary>
        /// Initializes the maximum number of async workers setting.
        /// </summary>
        private void InitializeMaximumNumberOfAsyncWorkers()
        {
            try
            {
                _maximumNumberOfAsyncWorkers = Settings.Default.MaximumNumberOfAsyncWorkers;
            }
            catch (InvalidCastException ex)
            {
                throw FlickrHelperException.GetInvalidConfigurationSettingException("MaximumNumberOfAsyncWorkers", ex);
            }

            if (_maximumNumberOfAsyncWorkers < 1)
            {
                _maximumNumberOfAsyncWorkers = 1;
            }
            else if (_maximumNumberOfAsyncWorkers > _MaximumConnectionsToFlickr)
            {
                _maximumNumberOfAsyncWorkers = _MaximumConnectionsToFlickr;
            }
        }

        /// <summary>
        /// Loads the list of all photosets of the user asynchronously.
        /// When the list becomes available, the <see cref="PhotoSets"/> property is set.
        /// </summary>
        private void LoadPhotosetsAsync()
        {
            WaitCallback loadPhotosets = delegate(object state)
            {
                EnsureConnected();

                try
                {
                    PhotosetCollection photosets = _flickrApi.PhotosetsGetList();
                    List<Photoset> photosetList = new List<Photoset>();
                    if (photosets != null)
                    {
                        photosetList.AddRange(photosets);
                    }

                    photosetList.Sort(delegate(Photoset set1, Photoset set2)
                       {
                           return String.Compare(set1.Title, set2.Title, StringComparison.CurrentCulture);
                       });

                    SetPhotosets(photosetList);
                }
                catch (WebException ex)
                {
                    throw FlickrHelperException.GetFailedToConnectToFlickrException(ex);
                }
                catch (FlickrException ex)
                {
                    throw HandleFlickrException(ex, Messages.GetPhotosetsFailed);
                }
            };

            ThreadPool.QueueUserWorkItem(loadPhotosets);
        }

        /// <summary>
        /// Makes sure that a Flickr connection was established. 
        /// Throws <see cref="InvalidOperationException"/> when not connected.
        /// </summary>
        /// <exception cref="InvalidOperationException">When not connected to Flickr.</exception>
        private void EnsureConnected()
        {
            if (!IsConnected)
            {
                throw new InvalidOperationException(Messages.NotConnected);
            }
        }

        /// <summary>
        /// Notifies subscribed parties that a propery value has changed.
        /// </summary>
        /// <param name="propertyName">The name of the property for which the value has changed.</param>
        private void NotifyPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        /// <summary>
        /// Handles a <see cref="FlickrException"/> exception by wrapping it with a
        /// <see cref="FlickrHelperException"/>.
        /// </summary>
        /// <param name="ex">The exception object to wrap.</param>
        private FlickrHelperException HandleFlickrException(FlickrException ex)
        {
            return HandleFlickrException(ex, Messages.GeneralFlickrError);
        }

        /// <summary>
        /// Handles a <see cref="FlickrException"/> exception.
        /// </summary>
        /// <param name="ex">The exception object.</param>
        /// <param name="friendlyErrorMessage">A friendly error message.</param>
        /// <exception cref="FlickrHelperException">Wraps the <see cref="FlickrException"/>.</exception>
        private FlickrHelperException HandleFlickrException(FlickrException ex, string friendlyErrorMessage)
        {
            FlickrHelperException wrappedEx = FlickrHelperException.WrapFlickrException(ex, friendlyErrorMessage);

            if (wrappedEx.Code == FlickrErrorCodes.InvalidAuthorizationToken)
            {
                ForgetAuthorizationToken();
            }

            return wrappedEx;
        }

        /// <summary>
        /// Calculates the number of workers to create and returns an array of instances.
        /// </summary>
        /// <param name="numberOfPictures">The total number of pictures to work on.</param>
        /// <param name="createAsyncFlickrWorker">A delegate to create a worker instance of type <typeparamref name="T"/>.</param>
        /// <typeparam name="T">The type of worker to create.</typeparam>
        /// <returns>An array of <typeparamref name="TWorker"/> instances.</returns>
        private AsyncFlickrWorker<TIn, TOut>[] GetAsyncFlickrWorkers<TIn, TOut>(int numberOfPictures, CreateAsyncFlickrWorker<TIn, TOut> createAsyncFlickrWorker)
        {
            int numberOfAsyncWorkers = Math.Min((numberOfPictures / _asyncWorkerPictureThreshold) + 1, _maximumNumberOfAsyncWorkers);

            AsyncFlickrWorker<TIn, TOut>[] workers = new AsyncFlickrWorker<TIn, TOut>[numberOfAsyncWorkers];

            for (int index = 0; index < numberOfAsyncWorkers; index++)
            {
                workers[index] = createAsyncFlickrWorker();
            }

            return workers;
        }

        /// <summary>
        /// Splits the specified list up in the specified number of lists.
        /// </summary>
        /// <param name="originalList">The original list.</param>
        /// <param name="numberOfLists">The number of lists to split up in.</param>
        /// <returns>An array of lists.</returns>
        private IList<T>[] GetSplittedLists<T>(IList<T> originalList, int numberOfLists)
        {
            List<T>[] splittedLists = new List<T>[numberOfLists];

            int capacityPerList = (originalList.Count / numberOfLists) + 1;
            for (int index = 0; index < numberOfLists; index++)
            {
                splittedLists[index] = new List<T>(capacityPerList);
            }

            // Split up the original list in round-robin fashion.
            int sequenceNumber = 0;
            foreach (T item in originalList)
            {
                int index = sequenceNumber % numberOfLists;

                splittedLists[index].Add(item);

                sequenceNumber++;
            }

            return splittedLists;
        }

        /// <summary>
        /// Processes a list of photos with multiple async workers and returns the result.
        /// </summary>
        /// <param name="metadataInList">The list with metadata of photos that should be processed.</param>
        /// <param name="progressCallback">A callback to receive progress information.</param>
        /// <param name="workerFactoryMethod">A factory method that can be used to create a worker instance.</param>
        /// <typeparam name="TIn">The "in" metadata type for the worker.</typeparam>
        /// <typeparam name="TOut">The "out" metadata type for the worker.</typeparam>
        /// <returns>A list with the metadata result of processing <paramref name="metadataInList"/>.</returns>
        private IList<TOut> ProcessMetadataWithMultipleWorkers<TIn, TOut>(
            IList<TIn> metadataInList,
            EventHandler<PictureProgressEventArgs> progressCallback,
            CreateAsyncFlickrWorker<TIn, TOut> workerFactoryMethod)
        {
            if (metadataInList == null)
            {
                throw new ArgumentNullException("metadataInList");
            }
            if (progressCallback == null)
            {
                throw new ArgumentNullException("progressCallback");
            }

            EnsureConnected();
            int totalNumberOfPhotos = metadataInList.Count;

            AsyncFlickrWorker<TIn, TOut>[] workers = GetAsyncFlickrWorkers(totalNumberOfPhotos, workerFactoryMethod);
            int numberOfAsyncWorkers = workers.Length;

            IList<TIn>[] listsOfMetadata = GetSplittedLists(metadataInList, numberOfAsyncWorkers);

            List<TOut> resultList = new List<TOut>(totalNumberOfPhotos);

            object resultLock = new Object();

            int numberOfPhotosProcessed = 0;
            bool shouldCancel = false;
            Exception error = null;

            // Create the callback
            EventHandler<AsyncFlickrWorkerEventArgs<TOut>> resultCallback = 
                delegate(object sender, AsyncFlickrWorkerEventArgs<TOut> fetchEventArgs)
            {
                lock (resultLock)
                {
                    numberOfPhotosProcessed++;

                    if (fetchEventArgs.Error != null)
                    {
                        shouldCancel = true;

                        // Only save the first error
                        if (error == null)
                        {
                            error = fetchEventArgs.Error;
                            if (error is FlickrException)
                            {
                                error = HandleFlickrException(error as FlickrException, Messages.GetPhotoInfoFailed);
                            }
                        }
                    }
                    else if (fetchEventArgs.Metadata != null)
                    {
                        resultList.Add(fetchEventArgs.Metadata);
                    }

                    PictureProgressEventArgs progressEventArgs = new PictureProgressEventArgs(numberOfPhotosProcessed, totalNumberOfPhotos);
                    progressCallback(this, progressEventArgs);
                    if (progressEventArgs.CancelProcess)
                    {
                        shouldCancel = true;
                    }

                    if (shouldCancel)
                    {
                        fetchEventArgs.Cancel = true;
                    }
                }
            };

            // Start the async workers.
            WaitHandle[] waitHandles = new WaitHandle[numberOfAsyncWorkers];
            for (int workerIndex = 0; workerIndex < numberOfAsyncWorkers; workerIndex++)
            {
                waitHandles[workerIndex] = workers[workerIndex].BeginWork(listsOfMetadata[workerIndex], resultCallback);
            }

            WaitHandle.WaitAll(waitHandles);

            if (error != null)
            {
                throw error;
            }

            return resultList.AsReadOnly();
        }
        #endregion

        #region INotifyPropertyChanged Members
        /// <summary>
        /// Event to notify of changes in property values.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;
        #endregion
    }
}
